﻿
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Humanizer.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.Web.CodeGeneration.Design;
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity;
using Mysqlx.Crud;
using Mysqlx.Prepare;
using OpenIddict.Client;
using Quartz;
using SageKing.Core.Extensions;
using SageKing.Core.Options;
using SageKing.Database.EF.Features;
using SageKing.Extensions;
using SageKing.OpenIddict.OpenIdService;
using SageKing.OpenIddict.Services.BaseService;
using SageKing.OpenIddict.UI.Api;
using SageKing.OpenIddict.UI.Identity.Api;
using SageKing.OpenIddict.UI.Infrastructure.Data;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationConstants;

namespace SageKing.OpenIddict.Features;


[DependsOn(typeof(SageKingDatabaseEFFeature))]
public class OpenIddictServerFeature : FeatureBase
{
    public OpenIddictServerFeature(IModule module) : base(module)
    {
    }

    public OpenIdServerOptions OpenIdOptions { get; set; }
    public Action<IdentityOptions> IdentityOptionsAction { get; set; } = _ => { };

    /// <inheritdoc />
    public override void Apply(IConfigurationManager configurationManager, IHostEnvironment hostEnvironment)
    {
        ArgumentNullException.ThrowIfNull(configurationManager);

        IdentityOptionsAction = (a) => { configurationManager.Bind(a, "IdentityOptions"); };

        if (OpenIdOptions == null)
        {
            OpenIdOptions = new OpenIdServerOptions();
            configurationManager.Bind(OpenIdOptions, OpenIdOptions.SectionName);
        }

        var AssemblyName = Module.CallAssembly.GetName().Name;
        OpenIdOptions.MigrationsAssembly = OpenIdOptions.MigrationsAssembly.IsNullOrEmptyToDefault(AssemblyName);

        Services.AddRouting(options =>
        {
            options.LowercaseUrls = true;
        });

        Services.AddDbContext<AppDbContext>(options =>
        {
            UseDataServer(options, OpenIdOptions);
        });

        // Register the Identity services.
        Services.AddIdentity<AppUser, AppRole>(options =>
        {
            options.User.RequireUniqueEmail = true;
        })
        .AddEntityFrameworkStores<AppDbContext>()
        .AddDefaultTokenProviders();

        // Configure Identity to use the same JWT claims as OpenIddict instead
        // of the legacy WS-Federation claims it uses by default (ClaimTypes),
        // which saves you from doing the mapping in your authorization controller.
        Services.Configure<IdentityOptions>(IdentityOptionsAction);

        if (!OpenIdOptions.IsTestingEnvironment)
        {
            // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
            // (like pruning orphaned authorizations/tokens from the database) at regular intervals.
            Services.AddQuartz(options =>
            {
                options.UseSimpleTypeLoader();
                options.UseInMemoryStore();
            });

            // Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
            Services.AddQuartzHostedService(options =>
            {
                options.WaitForJobsToComplete = true;
            });

        }

        Services.AddOpenIddict()
         // Register the OpenIddict core components.
         .AddCore(options =>
         {
             // Configure OpenIddict to use the Entity Framework Core stores and models.
             // Note: call ReplaceDefaultEntities() to replace the default entities.
             options.UseEntityFrameworkCore();

             // Enable Quartz.NET integration.
             options.UseQuartz();
         })
         // Register the OpenIddict server components.
         .AddServer(options =>
         {

             options.SetIssuer(new Uri(OpenIdOptions.Issuer)); 

             // Enable the authorization, device, logout, token, userinfo and verification endpoints.
             options.SetAuthorizationEndpointUris("connect/authorize")
                .SetEndSessionEndpointUris("connect/logout")
                .SetTokenEndpointUris("connect/token")
                .SetIntrospectionEndpointUris("connect/introspect")
                .SetUserInfoEndpointUris("connect/userinfo");

             // Enable the client credentials flow.
             options.AllowClientCredentialsFlow();

             // Enable the password flow.
             options.AllowPasswordFlow();

             // Note: this sample uses the code, device, password and refresh token flows, but you
             // can enable the other flows if you need to support implicit or client credentials.
             options.AllowAuthorizationCodeFlow()
                .AllowRefreshTokenFlow();


             // Accept anonymous clients (i.e clients that don't send a client_id).
             //options.AcceptAnonymousClients();

             // Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
             options.RegisterScopes(
                   Scopes.OpenId,
                   Scopes.Email,
                   Scopes.Profile,
                   Scopes.Roles,
                   "server_scope",
                   "api_scope"
              );

             if (hostEnvironment.IsDevelopment())
             {
                 // Register the signing and encryption credentials.
                 options.AddDevelopmentEncryptionCertificate()
                    .AddDevelopmentSigningCertificate();
             }
             else
             {
                 options.AddEphemeralEncryptionKey()
                    .AddEphemeralSigningKey();
             }

             // Accept anonymous clients (i.e clients that don't send a client_id).
             // options.AcceptAnonymousClients();

             // Force client applications to use Proof Key for Code Exchange (PKCE).
             options.RequireProofKeyForCodeExchange();

             // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
             options.UseAspNetCore()
                    .EnableTokenEndpointPassthrough()
                    .EnableEndUserVerificationEndpointPassthrough()
                    .EnableUserInfoEndpointPassthrough()
                    .DisableTransportSecurityRequirement(); // <-- 使用 nginx (反向代理处理 HTTPS)

             if (!OpenIdOptions.DisableAccessTokenEncryption)
             {
                 options.DisableAccessTokenEncryption();
             }

         }) // Register the OpenIddict validation components.
         .AddValidation(options =>
          {
              // Note: the validation handler uses OpenID Connect discovery
              // to retrieve the address of the introspection endpoint.
              options.SetIssuer(new Uri(OpenIdOptions.Issuer));

              options.AddAudiences(OpenIdOptions.ResourceServer.ClientId);

              // Configure the validation handler to use introspection and register the client
              // credentials used when communicating with the remote introspection endpoint.
              options.UseIntrospection()
                     .SetClientId(OpenIdOptions.ResourceServer.ClientId)
                     .SetClientSecret(OpenIdOptions.ResourceServer.ClientSecret);

              // Register the System.Net.Http integration.
              options.UseSystemNetHttp();

              // Register the ASP.NET Core host.
              options.UseAspNetCore();
          })
          // Register the EF based UI Store for OpenIddict related entities.
          .AddUIStore(options =>
          {
              options.OpenIddictUIContext = builder =>
              {
                  UseDataServer(builder, OpenIdOptions);
              };
          })
          // Register the Template Store for templates.
          .AddTemplateStore(options =>
          {
              options.TemplateContext = builder =>
              {
                  UseDataServer(builder, OpenIdOptions);
              };
          })
          // Register the APIs for the EF based UI Store based on OpenIddict.
          .AddUIApis(options =>
          {
              
              // Tell the system about the allowed Permissions it is built/configured for.
              options.Permissions =
              [
                  Permissions.Endpoints.Authorization,
                  Permissions.Endpoints.EndSession,
                  Permissions.Endpoints.Token,
                  Permissions.Endpoints.Introspection,
                  Permissions.GrantTypes.AuthorizationCode,
                  Permissions.GrantTypes.DeviceCode,
                  Permissions.GrantTypes.RefreshToken,
                  Permissions.ResponseTypes.Code,
                  Permissions.Scopes.Email,
                  Permissions.Scopes.Profile,
                  Permissions.Scopes.Roles,
                  Permissions.Prefixes.Scope + "server_scope",
                  Permissions.Prefixes.Scope + "api_scope"
              ];
          })
          // Register the EF based UI Store for the ASP.NET Identity related entities.
          .AddUIIdentityStore<AppUser>(options =>
          {
              options.OpenIddictUIIdentityContext = builder =>
              {
                  UseDataServer(builder, OpenIdOptions);
              };
          })
          // Register the APIs for the EF based UI Store based on ASP.NET Identity.
          .AddUIIdentityApis<AppUser>();

        Services.AddScoped<IMigrationService, MigrationService>();
        Services.AddScoped<ApplicationApiBaseService>();

        Services.AddMvc();

    }


    public override void Use(IHost app)
    {
        if (app is WebApplication webApp)
        {
            webApp.UseSageKingTenantMiddleware();
        }
    }

    private static void UseDataServer(DbContextOptionsBuilder options, OpenIdServerOptions tmpOpenIdOptions)
    {
        // Configure the context to use Microsoft SQL Server.
        switch (tmpOpenIdOptions.DBType)
        {
            case DBTypeEnum.SqlServer:
                options.UseSqlServer(tmpOpenIdOptions.ConnectionString,
                sqlOptions =>
                {
                    sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: tmpOpenIdOptions.MaxRetryCount,
                    maxRetryDelay: TimeSpan.FromSeconds(tmpOpenIdOptions.MaxRetryDelay),
                    errorNumbersToAdd: null
                    );
                    sqlOptions.MigrationsAssembly(tmpOpenIdOptions.MigrationsAssembly);
                });
                break;
            case DBTypeEnum.InMemory:
                options.UseInMemoryDatabase(tmpOpenIdOptions.ConnectionString);
                break;
            case DBTypeEnum.Sqlite:
                options.UseSqlite(tmpOpenIdOptions.ConnectionString, sqlOptions =>
                {
                    sqlOptions.MigrationsAssembly(tmpOpenIdOptions.MigrationsAssembly);
                });
                break;
            case DBTypeEnum.MySql:
                options.UseMySQL(tmpOpenIdOptions.ConnectionString, sqlOptions =>
                {
                    sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: tmpOpenIdOptions.MaxRetryCount,
                    maxRetryDelay: TimeSpan.FromSeconds(tmpOpenIdOptions.MaxRetryDelay),
                    errorNumbersToAdd: null
                    );
                    sqlOptions.MigrationsAssembly(tmpOpenIdOptions.MigrationsAssembly);
                });
                break;
            case DBTypeEnum.PostgreSQL:
                options.UseNpgsql(tmpOpenIdOptions.ConnectionString, sqlOptions =>
                {
                    sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: tmpOpenIdOptions.MaxRetryCount,
                    maxRetryDelay: TimeSpan.FromSeconds(tmpOpenIdOptions.MaxRetryDelay),
                     errorCodesToAdd: null
                    );
                    sqlOptions.MigrationsAssembly(tmpOpenIdOptions.MigrationsAssembly);
                });
                break;
            default:
                break;
        }
    }

}
